package game.jetclan.main;

import com.jme3.app.Application;
import com.jme3.app.state.AbstractAppState;
import com.jme3.app.state.AppStateManager;
import com.jme3.bounding.BoundingVolume;
import com.jme3.input.InputManager;
import com.jme3.input.KeyInput;
import com.jme3.input.MouseInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.AnalogListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.input.controls.MouseAxisTrigger;
import com.jme3.input.controls.MouseButtonTrigger;
import com.jme3.math.FastMath;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;

/**
 *
 *
 */
public class RtsCam extends AbstractAppState {

    /**
     * Degree of Freedom
     *
     */
    public enum DoF {

        SIDE,
        FWD,
        ROTATE,
        TILT,
        DISTANCE
    }

    public enum UpVector {

        Y_UP(Vector3f.UNIT_Y),
        Z_UP(Vector3f.UNIT_Z);
        final Vector3f upVector;

        UpVector(Vector3f upVector) {
            this.upVector = upVector;
        }
    }

    interface HeightProvider {

        public float getHeight(Vector2f coord);
    }
    private boolean debug = false;
    private InputManager inputManager;
    private Camera cam; 
    private BoundingVolume centerBounds;
    private BoundingVolume cameraBounds;
    private int screenResWidth = -1;
    private int screenResHeight = -1;
    private int autoScrollXLeftOffset = 0;
    private int autoScrollXRightOffset = 0;
    private int autoScrollTopOffset = 0;
    private int autoScrollBottomOffset = 0;
    private static final int AUTO_SCROLL_OFFSET = 5;
    private final int[] direction = new int[5];
    private final float[] accelTime = new float[5];
    private final float[] offsetMoves = new float[5];
    private final float[] maxSpeedPerSecondOfAccell = new float[5];
    private final float[] maxAccellPeriod = new float[5];
    private final float[] minValue = new float[5];
    private final float[] maxValue = new float[5];
    public final Vector3f position = new Vector3f();
    public final Vector3f center = new Vector3f();
    private final InternalListener listener = new InternalListener();
    private final UpVector up;
    private final Vector3f oldPosition = new Vector3f();
    private final Vector3f oldCenter = new Vector3f();
    private float tilt = FastMath.PI / 4;
    private float rot = -FastMath.PI;
    private float distance = 10;
    private HeightProvider heightProvider;
    private boolean wheelEnabled = true;
    //private String mouseRotationButton = "BUTTON3";
    //private String mouseDragButton = "BUTTON1";
    //private boolean mouseRotation;
    private boolean mouseDrag;
    private static final int SIDE = DoF.SIDE.ordinal();
    private static final int FWD = DoF.FWD.ordinal();
    private static final int ROTATE = DoF.ROTATE.ordinal();
    private static final int TILT = DoF.TILT.ordinal();
    private static final int DISTANCE = DoF.DISTANCE.ordinal();
    private static final float WHEEL_SPEED = 3f / 15;
    private static String[] mappings = new String[]{
        "+SIDE", "+FWD", "+ROTATE", "+TILT", "+DISTANCE", "-SIDE", "-FWD", "-ROTATE", "-TILT", "-DISTANCE", "+WHEEL", "-WHEEL", "-MOUSEX", "+MOUSEX", "-MOUSEY", "+MOUSEY"};
//        "BUTTON1", "BUTTON2", "BUTTON3"};

    public RtsCam(UpVector up, int screenResWidth, int screenResHeight) {
        this.up = up;
        //Setup auto scroll
        this.screenResWidth = screenResWidth;
        this.screenResHeight = screenResHeight;

        autoScrollXLeftOffset = AUTO_SCROLL_OFFSET;
        autoScrollXRightOffset = this.screenResWidth - AUTO_SCROLL_OFFSET;
        autoScrollTopOffset = this.screenResHeight - AUTO_SCROLL_OFFSET;
        autoScrollBottomOffset = AUTO_SCROLL_OFFSET;

        //setMinMaxValues(DoF.SIDE, Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY);
        setMinMaxValues(DoF.SIDE, -365, 365);
        setMinMaxValues(DoF.FWD, -340, 340);
        setMinMaxValues(DoF.ROTATE, 0, 0);
        setMinMaxValues(DoF.TILT,
                0.2f, (float) (Math.PI
                / 2) - 0.001f);
        setMinMaxValues(DoF.DISTANCE,
                50, 800);
        setMaxSpeed(DoF.SIDE,
                10f, 0.4f);
        setMaxSpeed(DoF.FWD,
                10f, 0.4f);
        setMaxSpeed(DoF.ROTATE,
                2f, 0.4f);
        setMaxSpeed(DoF.TILT,
                1f, 0.4f);
        setMaxSpeed(DoF.DISTANCE,
                15f, 0.4f);
    }

    public RtsCam(UpVector up, Float sideMin, Float sideMax, Float fwdMin, Float fwdMax, Float tiltMin, Float tiltMax, Float distanceMin, Float distanceMax) {
        this.up = up;

        setMinMaxValues(DoF.SIDE, sideMin, sideMax);
        setMinMaxValues(DoF.FWD, fwdMin, fwdMax);
        setMinMaxValues(DoF.ROTATE, Float.NEGATIVE_INFINITY, Float.POSITIVE_INFINITY);
        setMinMaxValues(DoF.TILT, 0.2f, (float) (Math.PI / 2) - 0.001f);
        setMinMaxValues(DoF.DISTANCE, distanceMin, distanceMax);

        setMaxSpeed(DoF.SIDE, 10f, 0.4f);
        setMaxSpeed(DoF.FWD, 10f, 0.4f);
        setMaxSpeed(DoF.ROTATE, 2f, 0.4f);
        setMaxSpeed(DoF.TILT, 1f, 0.4f);
        setMaxSpeed(DoF.DISTANCE, 15f, 0.4f);
    }

    @Override
    public void initialize(AppStateManager stateManager, Application app) {
        super.initialize(stateManager, app);
        this.cam = app.getCamera();
        this.inputManager = app.getInputManager();
        registerWithInput(inputManager);
    }

    /**
     * Set the maximum speed for given direction of movement. For
     * SIDE/FWD/DISTANCE it is in units/second, for ROTATE/TILT it is in
     * radians/second
     *
     * @param deg degree of freedom for which to set the maximum speed
     * @param maxSpd maximum speed of movement in that direction
     * @param accelTime amount of time which is need to accelerate to full speed
     * in seconds (has to be bigger than zero, values over half second feel very
     * sluggish). Defaults are 0.4 seconds
     */
    public void setMaxSpeed(DoF deg, float maxSpd, float accelTime) {
        maxSpeedPerSecondOfAccell[deg.ordinal()] = maxSpd / accelTime;
        maxAccellPeriod[deg.ordinal()] = accelTime;
    }

    /**
     * Set the terrain following logic for camera. Camera position will not get
     * under the value returned by the heightProvider. Please add some extra
     * buffering here, so camera will not clip the actual terrain – for example
     *
     * new HeightProvider() {
     *
     * @Override public float getHeight(Vector2f coord) { return
     * terrain.getHeight(coord) + 10; } }
     *
     * @param heightProvider
     */
    public void setHeightProvider(HeightProvider heightProvider) {
        this.heightProvider = heightProvider;
    }

    /**
     * Enables/disabled wheel-zoom behaviour Default is enabled
     */
    public void setWheelEnabled(boolean wheelEnabled) {
        this.wheelEnabled = wheelEnabled;
    }

//    private String mouseButtonName(int button) {
//        switch (button) {
//            case MouseInput.BUTTON_LEFT:
//                return "BUTTON1";
//
//            case MouseInput.BUTTON_MIDDLE:
//                return "BUTTON2";
//
//            case MouseInput.BUTTON_RIGHT:
//                return "BUTTON3";
//            default:
//                return null;
//        }
//    }

//    /**
//     * Use MouseInput.BUTTON_ constants to indicate which buttons should be used
//     * for rotation and dragging with mouse Defaults are BUTTON_MIDDLE for
//     * rotation and BUTTON_RIGHT for dragging Use -1 to disable given
//     * functionality
//     *
//     * @param rotationButton button to hold to control TILT/ROTATION with mouse
//     * movements
//     * @param dragButton button to hold to drag camera position around
//     */
//    public void setMouseDragging(int rotationButton, int dragButton) {
//        mouseDragButton = mouseButtonName(dragButton);
//        mouseRotationButton = mouseButtonName(rotationButton);
//    }

    public void update(final float tpf) {
        
        doAutoScroll();
        for (int i = 0; i < direction.length; i++) {
            int dir = direction[i];
            switch (dir) {
                case -1:
                    accelTime[i] = clamp(-maxAccellPeriod[i], accelTime[i] - tpf, accelTime[i]);
                    break;
                case 0:
                    if (accelTime[i] != 0) {
                        double oldSpeed = accelTime[i];
                        if (accelTime[i] > 0) {
                            accelTime[i] -= tpf;
                        } else {
                            accelTime[i] += tpf;
                        }
                        if (oldSpeed * accelTime[i] < 0) {
                            accelTime[i] = 0;
                        }
                    }
                    break;
                case 1:
                    accelTime[i] = clamp(accelTime[i], accelTime[i] + tpf, maxAccellPeriod[i]);
                    break;
            }

        }

        float distanceChange = maxSpeedPerSecondOfAccell[DISTANCE] * accelTime[DISTANCE] * tpf;
        distance += distanceChange;
        distance += offsetMoves[DISTANCE];

        tilt += maxSpeedPerSecondOfAccell[TILT] * accelTime[TILT] * tpf + offsetMoves[TILT];
        rot += maxSpeedPerSecondOfAccell[ROTATE] * accelTime[ROTATE] * tpf + offsetMoves[ROTATE];

        distance = clamp(minValue[DISTANCE], distance, maxValue[DISTANCE]);
        rot = clamp(minValue[ROTATE], rot, maxValue[ROTATE]);
        tilt = clamp(minValue[TILT], tilt, maxValue[TILT]);

        double offX = maxSpeedPerSecondOfAccell[SIDE] * accelTime[SIDE] * tpf + offsetMoves[SIDE];
        double offY = maxSpeedPerSecondOfAccell[FWD] * accelTime[FWD] * tpf + offsetMoves[FWD];

        if (up == UpVector.Y_UP) {
            center.x += offX * Math.cos(-rot) + offY * Math.sin(rot);
            center.x = clamp(minValue[SIDE], center.x, maxValue[SIDE]);
            center.z += offX * Math.sin(-rot) + offY * Math.cos(rot);
            center.z = clamp(minValue[FWD], center.z, maxValue[FWD]);
            
        } else {
            center.x += offX * Math.cos(-rot) + offY * Math.sin(rot);
            center.x = clamp(minValue[SIDE], center.x, maxValue[SIDE]);
            center.y += offX * Math.sin(-rot) + offY * Math.cos(rot);
            center.z = clamp(minValue[FWD], center.z, maxValue[FWD]);
        }

        if (centerBounds != null) {
            //TODO: clamp center to bounds
        }

        if (up == UpVector.Y_UP) {
            position.x = center.x + (float) (distance * Math.cos(tilt) * Math.sin(rot));
            //position.x = clamp(minValue[SIDE], position.x, maxValue[SIDE]);
            position.y = center.y + (float) (distance * Math.sin(tilt));
            position.z = center.z + (float) (distance * Math.cos(tilt) * Math.cos(rot));
            if (heightProvider != null) {
                float h = heightProvider.getHeight(new Vector2f(position.x, position.z));
                if (position.y < h) {
                    position.y = h;
                }
            }
        } else {
            position.x = center.x + (float) (distance * Math.cos(tilt) * Math.sin(rot));
            position.y = center.y + (float) (distance * Math.cos(tilt) * Math.cos(rot));
            position.z = center.z + (float) (distance * Math.sin(tilt));
            if (heightProvider != null) {
                float h = heightProvider.getHeight(new Vector2f(position.x, position.y));
                if (position.z < h) {
                    position.z = h;
                }
            }
        }

        for (int i = 0; i < offsetMoves.length; i++) {
            offsetMoves[i] = 0;
        }

        if (oldPosition.equals(position) && oldCenter.equals(center)) {
            return;
        }

        if (cameraBounds != null) {
            //TODO: clamp position to bounds
        }

        cam.setLocation(position);
        cam.lookAt(center, up.upVector);

        oldPosition.set(position);
        oldCenter.set(center);


        if (debug) {
            System.out.print(debugKeyCamValues());
        }

    }

    private static float clamp(float min, float value, float max) {
        if (value < min) {
            return min;
        } else if (value > max) {
            return max;
        } else {
            return value;
        }
    }

    public float getMaxSpeed(DoF dg) {
        return maxSpeedPerSecondOfAccell[dg.ordinal()];
    }

    public float getMinValue(DoF dg) {
        return minValue[dg.ordinal()];
    }

    public float getMaxValue(DoF dg) {
        return maxValue[dg.ordinal()];
    }

    /**
     * SIDE and FWD min/max values are ignored
     *
     * @param dg
     * @param min
     * @param max
     */
    public void setMinMaxValues(DoF dg, float min, float max) {
        minValue[dg.ordinal()] = min;
        maxValue[dg.ordinal()] = max;
    }

    public Vector3f getPosition() {
        return position;
    }

    public void setCenter(Vector3f center) {
        this.center.set(center);
    }

    public Vector3f getCenter() {
        return center;
    }

    public float getDistance() {
        return distance;
    }

    public float getRot() {
        return rot;
    }

    public float getTilt() {
        return tilt;
    }

    public void setDistance(float distance) {
        this.distance = distance;
    }

    public void setRot(float rot) {
        this.rot = rot;
    }

    public void setTilt(float tilt) {
        this.tilt = tilt;
    }

    public Camera getCamera() {
        return cam;
    }

    private void registerWithInput(InputManager inputManager) {
        this.inputManager = inputManager;

        if (up == UpVector.Y_UP) {
            inputManager.addMapping("-SIDE", new KeyTrigger(KeyInput.KEY_A));
            inputManager.addMapping("+SIDE", new KeyTrigger(KeyInput.KEY_D));
            inputManager.addMapping("+ROTATE", new KeyTrigger(KeyInput.KEY_Q));
            inputManager.addMapping("-ROTATE", new KeyTrigger(KeyInput.KEY_E));
        } else {
            inputManager.addMapping("+SIDE", new KeyTrigger(KeyInput.KEY_A));
            inputManager.addMapping("-SIDE", new KeyTrigger(KeyInput.KEY_D));
            inputManager.addMapping("-ROTATE", new KeyTrigger(KeyInput.KEY_Q));
            inputManager.addMapping("+ROTATE", new KeyTrigger(KeyInput.KEY_E));
        }

        inputManager.addMapping("+FWD", new KeyTrigger(KeyInput.KEY_S));
        inputManager.addMapping("-FWD", new KeyTrigger(KeyInput.KEY_W));

        inputManager.addMapping("+TILT", new KeyTrigger(KeyInput.KEY_R));
        inputManager.addMapping("-TILT", new KeyTrigger(KeyInput.KEY_F));
        inputManager.addMapping("-DISTANCE", new KeyTrigger(KeyInput.KEY_Z));
        inputManager.addMapping("+DISTANCE", new KeyTrigger(KeyInput.KEY_X));

        inputManager.addMapping("-WHEEL", new MouseAxisTrigger(MouseInput.AXIS_WHEEL, false));
        inputManager.addMapping("+WHEEL", new MouseAxisTrigger(MouseInput.AXIS_WHEEL, true));

        inputManager.addMapping("-MOUSEX", new MouseAxisTrigger(MouseInput.AXIS_X, false));
        inputManager.addMapping("+MOUSEX", new MouseAxisTrigger(MouseInput.AXIS_X, true));
        inputManager.addMapping("-MOUSEY", new MouseAxisTrigger(MouseInput.AXIS_Y, false));
        inputManager.addMapping("+MOUSEY", new MouseAxisTrigger(MouseInput.AXIS_Y, true));

//        inputManager.addMapping("BUTTON1", new MouseButtonTrigger(MouseInput.BUTTON_LEFT));
//        inputManager.addMapping("BUTTON2", new MouseButtonTrigger(MouseInput.BUTTON_MIDDLE));
//        inputManager.addMapping("BUTTON3", new MouseButtonTrigger(MouseInput.BUTTON_RIGHT));

        inputManager.addListener(listener, mappings);
    }

    @Override
    public void cleanup() {
        super.cleanup();

        for (String mapping : mappings) {
            if (inputManager.hasMapping(mapping)) {
                inputManager.deleteMapping(mapping);
            }
        }
        inputManager.removeListener(listener);


    }

    private class InternalListener implements ActionListener, AnalogListener {

        public void onAction(String name, boolean isPressed, float tpf) {
            if (!isEnabled()) {
                return;
            }

            int press = isPressed ? 1 : 0;

            if (name.contains("WHEEL") || name.contains("MOUSE")) {
                return;
            }

//            if (name.equals(mouseRotationButton)) {
//                mouseRotation = isPressed;
//                inputManager.setCursorVisible(!mouseDrag && !mouseRotation);
//                return;
//            }
//
//            if (name.equals(mouseDragButton)) {
//                mouseDrag = isPressed;
//                inputManager.setCursorVisible(!mouseDrag && !mouseRotation);
//                return;
//            }

            char sign = name.charAt(0);
            if (sign == '-') {
                press = -press;
            } else if (sign != '+') {
                return;
            }

            DoF deg = DoF.valueOf(name.substring(1));
            direction[deg.ordinal()] = press;
        }

        @Override
        public void onAnalog(String name, float value, float tpf) {


            if (!isEnabled()) {
                return;
            }

            if (!name.contains("WHEEL") && !name.contains("MOUSE")) {
                return;
            }

            char sign = name.charAt(0);
            if (sign == '-') {
                value = -value;
            } else if (sign != '+') {
                return;
            }

            if (name.contains("WHEEL")) {
                if (!wheelEnabled) {
                    return;
                }
                float speed = maxSpeedPerSecondOfAccell[DISTANCE] * maxAccellPeriod[DISTANCE] * WHEEL_SPEED;
                offsetMoves[DISTANCE] += value * speed;
            //            } else if (name.contains("MOUSE")) {
//                if (mouseRotation) {
//                    int direction;
//                    if (name.endsWith("X")) {
//                        direction = ROTATE;
//                        if (up == UpVector.Z_UP) {
//                            value = -value;
//                        }
//                    } else {
//                        direction = TILT;
//                    }
//                    offsetMoves[direction] += value;
//                } else if (mouseDrag) {
//                    int direction;
//                    if (name.endsWith("X")) {
//                        direction = SIDE;
//                        if (up == UpVector.Z_UP) {
//                            value = -value;
//                        }
//                    } else {
//                        direction = FWD;
//                        value = -value;
//                    }
//                    offsetMoves[direction] += value * maxSpeedPerSecondOfAccell[direction] * maxAccellPeriod[direction];
//                }
            }
        }
    }

    private void doAutoScroll() {
        //Autoscroll to left
        if (inputManager.getCursorPosition().getX() < autoScrollXLeftOffset) {
            int direction = SIDE;
            offsetMoves[direction] += -0.05 * maxSpeedPerSecondOfAccell[direction] * maxAccellPeriod[direction];

        } //Autoscroll to right
        else if (inputManager.getCursorPosition().getX() > autoScrollXRightOffset) {
            int direction = SIDE;
            offsetMoves[direction] += 0.05 * maxSpeedPerSecondOfAccell[direction] * maxAccellPeriod[direction];
        }

        //Autoscroll to left
        if (inputManager.getCursorPosition().getY() < autoScrollBottomOffset) {
            int direction = FWD;
            offsetMoves[direction] += 0.05 * maxSpeedPerSecondOfAccell[direction] * maxAccellPeriod[direction];

        } //Autoscroll to right
        else if (inputManager.getCursorPosition().getY() > autoScrollTopOffset) {
            int direction = FWD;
            offsetMoves[direction] += -0.05 * maxSpeedPerSecondOfAccell[direction] * maxAccellPeriod[direction];
        }

    }

    /**
     * This is used for jumping the camera into a new position, for example when the user clicks on a minimap.
     * --- TODO SYNCHRONISE POSITION ---
     * @param x new X position value
     * @param y new Y position value
     */
    public void gotoPosition(int x, int z){
         
        center.x = clamp(minValue[SIDE], x, maxValue[SIDE]);        
        center.z = clamp(minValue[FWD], z, maxValue[FWD]);
        
         if (up == UpVector.Y_UP) {
            position.x = center.x + (float) (distance * Math.cos(tilt) * Math.sin(rot));
            //position.x = clamp(minValue[SIDE], position.x, maxValue[SIDE]);
            position.y = center.y + (float) (distance * Math.sin(tilt));
            position.z = center.z + (float) (distance * Math.cos(tilt) * Math.cos(rot));
            if (heightProvider != null) {
                float h = heightProvider.getHeight(new Vector2f(position.x, position.z));
                if (position.y < h) {
                    position.y = h;
                }
            }
        } else {
            position.x = center.x + (float) (distance * Math.cos(tilt) * Math.sin(rot));
            position.y = center.y + (float) (distance * Math.cos(tilt) * Math.cos(rot));
            position.z = center.z + (float) (distance * Math.sin(tilt));
            if (heightProvider != null) {
                float h = heightProvider.getHeight(new Vector2f(position.x, position.y));
                if (position.z < h) {
                    position.z = h;
                }
            }
        }
        
         
    }
    
    /**
     * Returns a string which contains some key values for the camer position
     * and such
     *
     * @return A debug string in order to track the camera
     */
    public String debugKeyCamValues() {
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append("position: " + position.toString() + "\n");
        stringBuffer.append("center: " + center.toString() + "\n");
        stringBuffer.append("tilt: " + tilt + "\n");
        stringBuffer.append("rot: " + rot + "\n");
        stringBuffer.append("distance: " + distance + "\n");
        return stringBuffer.toString();
    }
}